Skip to content

feat(ui): major UI/UX overhaul for forms and resource management#1

Open
IvanHunters wants to merge 40 commits intomainfrom
feat/ui-overhaul
Open

feat(ui): major UI/UX overhaul for forms and resource management#1
IvanHunters wants to merge 40 commits intomainfrom
feat/ui-overhaul

Conversation

@IvanHunters
Copy link
Copy Markdown

@IvanHunters IvanHunters commented Apr 24, 2026

Summary

Major UI/UX improvements for console application forms and resource management:

  • StorageClassWidget - automatic storage class selection dropdown
  • AdditionalPropertiesField - editor for fields with additionalProperties schema
  • Backup Management - complete CRUD UI for Backup, BackupPlan, and BackupRestoreJob resources
  • Form improvements - conditional rendering for addon objects, improved schema handling
  • Secrets display - collapsible sections with formatted output for large secrets
  • UI polish - replaced "Chart Values" with "Parameters", improved tenant modules detection

Key Features

Storage Class Widget

Automatically detects and provides dropdown for storage class selection in all forms with storageClass field.

Backup Management

Full backup workflow UI:

  • Create/edit/delete Backup resources
  • Create/edit/delete BackupPlan schedules
  • Create BackupRestoreJob for recovery
  • Dynamic forms generated from CRD schemas

Form Field Editors

  • AdditionalPropertiesEditor for map-like fields with structured schemas
  • Improved handling of free-form objects vs structured additionalProperties
  • Conditional rendering for addon configuration (show config only when enabled)

Secrets Display

  • Collapsible sections for large secrets
  • Formatted display for YAML/JSON content
  • Show/hide toggle with line count indication

Test Plan

  • Verify StorageClassWidget appears and works in VM/Postgres/etc forms
  • Test backup creation workflow end-to-end
  • Check additionalProperties fields render correctly (e.g., Backup includeNamespaces)
  • Verify addon conditional rendering in Kubernetes cluster forms
  • Test secrets display with various content types

Summary by CodeRabbit

  • New Features

    • Backup management system: create and manage backup plans, backup jobs, backups, and restore jobs
    • Interactive VNC console with live connection controls for virtual machine access
    • Advanced form controls for complex object properties and nested configurations
  • Improvements

    • Redesigned secrets interface with expandable grouping and reveal controls
    • Sidebar navigation layout optimization
    • Enhanced Kubernetes resource filtering for real-time synchronization

ohotnikov.ivan and others added 7 commits April 24, 2026 00:59
- Add CustomObjectFieldTemplate for free-form key-value objects
- Add KeyValueEditor component for Helm valuesOverride fields
- Add SourceWidget for mutually exclusive source selection (VM Disk)
- Add custom button templates (Add, Remove, MoveUp, MoveDown)
- Fix select dropdown arrow positioning with proper padding
- Install @novnc/novnc for VNC viewer support

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Signed-off-by: ohotnikov.ivan <ohotnikov.ivan@e-queo.net>
- Add pre-formatted text display for kubeconfig and large secrets
- Add collapse/expand functionality for large secrets
- Add dark terminal-like theme for better readability
- Show "Show more/Show less" button for secrets larger than 5 lines or 200 chars

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Signed-off-by: ohotnikov.ivan <ohotnikov.ivan@e-queo.net>
- VMDisk shows only Overview tab (no Workloads/Services/Ingresses/Secrets)
- VMInstance shows additional VNC tab
- Other application types show full set of tabs

Storage-only resources like VMDisk don't have workloads, services, or ingresses,
so hiding those tabs provides cleaner UX.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Signed-off-by: ohotnikov.ivan <ohotnikov.ivan@e-queo.net>
- Add VNC tab component with noVNC library integration
- Configure Vite proxy for WebSocket support (ws: true)
- Use dynamic import for @novnc/novnc to handle CommonJS module

Note: VNC WebSocket connection not working yet due to kubectl proxy limitations.
kubectl proxy doesn't support WebSocket upgrade for KubeVirt VNC subresources.
Temporary solution: use virtctl vnc command directly.
Future: implement backend proxy for proper WebSocket support.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Signed-off-by: ohotnikov.ivan <ohotnikov.ivan@e-queo.net>
- Fetch StorageClasses from Kubernetes API using useK8sList
- Display in select dropdown with (default) marker
- Auto-select default StorageClass if no value set
- Handle loading and empty states
- Register in customWidgets for RJSF forms

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Signed-off-by: ohotnikov.ivan <ohotnikov.ivan@e-queo.net>
- Add MutuallyExclusiveField component for radio button selection
- Remove unused StorageClassWidget
- Clean up rjsf-templates exports

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Signed-off-by: ohotnikov.ivan <ohotnikov.ivan@e-queo.net>
Watch requests were not filtering by labelSelector/fieldSelector,
causing all resources in namespace to be returned instead of only
those matching the filter.

- Add search parameter to client.watch() method
- Pass labelSelector and fieldSelector to watch URLSearchParams
- Update hooks to pass selectors from useK8sList to client.watch()

This fixes the issue where Secrets tab showed all namespace secrets
instead of only those belonging to the current application.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Signed-off-by: ohotnikov.ivan <ohotnikov.ivan@e-queo.net>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 24, 2026

Warning

Rate limit exceeded

@IvanHunters has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 16 minutes and 22 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 16 minutes and 22 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6d1803e0-b3cb-41a5-adce-38938d131c8b

📥 Commits

Reviewing files that changed from the base of the PR and between 7ad2875 and 14fe3c9.

📒 Files selected for processing (27)
  • apps/console/src/components/AdditionalPropertiesEditor.tsx
  • apps/console/src/components/AdditionalPropertiesField.tsx
  • apps/console/src/components/AdditionalPropertiesWidget.tsx
  • apps/console/src/components/CustomObjectFieldTemplate.tsx
  • apps/console/src/components/SchemaForm.tsx
  • apps/console/src/components/SourceField.tsx
  • apps/console/src/components/SourceWidget.tsx
  • apps/console/src/components/StorageClassWidget.tsx
  • apps/console/src/components/rjsf-templates.tsx
  • apps/console/src/lib/keys-order.ts
  • apps/console/src/lib/use-crd-schema.ts
  • apps/console/src/routes/ApplicationListPage.tsx
  • apps/console/src/routes/BackupCreatePage.tsx
  • apps/console/src/routes/BackupPlanCreatePage.tsx
  • apps/console/src/routes/BackupResourceCreatePage.tsx
  • apps/console/src/routes/BackupResourceCreatePageWithData.tsx
  • apps/console/src/routes/BackupResourceEditPage.tsx
  • apps/console/src/routes/BackupResourceListPage.tsx
  • apps/console/src/routes/BackupRestoreJobCreatePage.tsx
  • apps/console/src/routes/ConsolePage.tsx
  • apps/console/src/routes/TenantsPage.tsx
  • apps/console/src/routes/backup-schemas.ts
  • apps/console/src/routes/detail/ApplicationDetailPage.tsx
  • apps/console/src/routes/detail/SecretsTab.tsx
  • apps/console/src/routes/detail/VncTab.tsx
  • apps/console/src/routes/sidebar-sections.tsx
  • packages/ui/src/components/layout/Sidebar.tsx
📝 Walkthrough

Walkthrough

This pull request introduces comprehensive backup functionality to the console app, adding new form components for editing additional properties and mutually exclusive fields, backup-related CRUD routes, VNC client integration for VM instances, Kubernetes watch filtering support, and custom schema-driven form widgets to handle complex configuration objects.

Changes

Cohort / File(s) Summary
Package Dependencies
apps/console/package.json
Added @novnc/novnc dependency for VNC client support.
Form Editor Components
apps/console/src/components/AdditionalPropertiesEditor.tsx, KeyValueEditor.tsx, MutuallyExclusiveField.tsx
New React components for editing complex form properties: additional properties via per-key RJSF forms, key-value pairs with JSON parsing, and mutually exclusive radio-group-driven fields with nested inputs.
RJSF Field & Widget Components
apps/console/src/components/AdditionalPropertiesField.tsx, AdditionalPropertiesWidget.tsx, SourceWidget.tsx, StorageClassWidget.tsx
Custom RJSF field and widget implementations for handling additional properties, source/target selection with sub-properties, and Kubernetes storage class dropdowns with default class detection.
Schema Form Infrastructure
apps/console/src/components/SchemaForm.tsx, CustomObjectFieldTemplate.tsx, rjsf-templates.tsx, schema-form.css
Core form components: schema-driven form with dynamic widget/field registration, custom object field template for free-form/addon object detection, RJSF template registry with custom buttons, and adjusted textarea/select styling.
Schema Utilities
apps/console/src/lib/keys-order.ts, use-crd-schema.ts, backup-schemas.ts
Utilities for schema handling: CRD schema normalization and fetching via useCRDSchema hook, pre-defined JSON schemas for backup/restore resources, and title normalization ("Chart Values" → "Parameters").
Backup CRUD Routes
apps/console/src/routes/BackupCreatePage.tsx, BackupPlanCreatePage.tsx, BackupResourceCreatePage.tsx, BackupResourceCreatePageWithData.tsx, BackupResourceEditPage.tsx, BackupResourceListPage.tsx, BackupRestoreJobCreatePage.tsx
New page components for backup management: create/edit/list operations for plans, backups, backup jobs, and restore jobs; dynamic schema enrichment with enum values from Kubernetes resources and application definitions.
Route Configuration & Sidebar
apps/console/src/routes/ConsolePage.tsx, sidebar-sections.tsx
Added backup-related routes under /console/backups/* namespace and fixed "Backups" sidebar section with links to plans, jobs, backups, and restore jobs.
Application Detail Pages
apps/console/src/routes/detail/ApplicationDetailPage.tsx, SecretsTab.tsx, VncTab.tsx
Enhanced application detail view: dynamic tab configuration per resource type (VMInstance, VMDisk, etc.), refactored secrets UI with expandable groups and reveal-all control, new interactive noVNC client integration for VNC tab with connection lifecycle management.
Tenant & Module Management
apps/console/src/routes/TenantsPage.tsx
Updated tenant list to derive modules via TenantModule CRD queries instead of label-based lookup; added tenant edit action button.
Kubernetes Client Enhancements
packages/k8s-client/src/client.ts, hooks.ts
Added watch filtering support: K8sClient.watch now accepts optional labelSelector/fieldSelector; useK8sList propagates selectors to watch stream in addition to initial list request.
Layout Updates
packages/ui/src/components/layout/Sidebar.tsx
Converted sidebar to flex layout with scrollable navigation area (flex-col, flex-1, overflow-y-auto).

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant ConsoleApp as Console App
    participant SchemaForm as SchemaForm<br/>(with Schema<br/>Enrichment)
    participant K8sAPI as Kubernetes API
    participant AppDefs as App Definitions<br/>(Memory)

    User->>ConsoleApp: Navigate to Create Backup
    ConsoleApp->>K8sAPI: Fetch Backup CRD Schema
    K8sAPI-->>ConsoleApp: OpenAPI Schema
    ConsoleApp->>K8sAPI: Fetch ApplicationDefinitions
    K8sAPI-->>ConsoleApp: App Definitions List
    ConsoleApp->>K8sAPI: Fetch Application Instances
    K8sAPI-->>ConsoleApp: Instances List
    ConsoleApp->>SchemaForm: Render with Enriched Schema<br/>(enums from fetched data)
    SchemaForm-->>ConsoleApp: Schema Form UI
    ConsoleApp-->>User: Display Form
    User->>SchemaForm: Fill in Fields & Submit
    SchemaForm->>ConsoleApp: Validate & Prepare Backup CR
    ConsoleApp->>K8sAPI: Create Backup Resource
    K8sAPI-->>ConsoleApp: Resource Created
    ConsoleApp->>User: Navigate to Backups List
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Poem

🐰 Hops of joy through backup lands,
Schema forms with eager hands,
VNC streams and storage care,
Archive dreams now floating there! 🎉

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(ui): major UI/UX overhaul for forms and resource management' accurately describes the changeset's primary focus: comprehensive UI improvements to forms and resource management across the console application.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/ui-overhaul

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@IvanHunters IvanHunters marked this pull request as ready for review April 24, 2026 08:54
ohotnikov.ivan added 21 commits April 24, 2026 11:55
Create StorageClassWidget that dynamically loads storage classes from
Kubernetes API and displays them in a dropdown instead of text input.

- Add StorageClassWidget component with K8s API integration
- Register widget in RJSF custom widgets
- Auto-apply widget to all storageClass fields via uiSchema
- Remove duplicate label rendering (RJSF handles it)

Signed-off-by: ohotnikov.ivan <ohotnikov.ivan@e-queo.net>
- Add overflow scrolling to sidebar for long menus
- Fix TenantsPage to show Tenant CR from current namespace
- Add Edit button for each tenant linking to edit form
- Fix CustomObjectFieldTemplate to support additionalProperties as object
- Enable resourceQuotas field editing with KeyValueEditor

Signed-off-by: ohotnikov.ivan <ohotnikov.ivan@e-queo.net>
…ection

Add custom RJSF widget that automatically loads and displays available storage classes from the cluster. The widget:
- Fetches StorageClass resources from storage.k8s.io/v1 API
- Automatically selects default storage class if available
- Shows loading state during fetch
- Marks default storage class with (default) label
- Automatically applied to all 'storageClass' fields in schemas via recursive schema traversal

Signed-off-by: ohotnikov.ivan <ohotnikov.ivan@e-queo.net>
Fix RFB constructor import from @novnc/novnc CommonJS module. The module exports with nested default structure (module.default.default), which required using dynamic import with proper fallback chain to access the RFB constructor.

Signed-off-by: ohotnikov.ivan <ohotnikov.ivan@e-queo.net>
Update TenantsPage to use TenantNamespace resources directly from context instead of fetching Tenant ApplicationInstances. Display additional metadata:
- Namespace name
- Host label
- Enabled modules (etcd, ingress, monitoring, seaweedfs)

Remove edit action button and status column for cleaner table layout.

Signed-off-by: ohotnikov.ivan <ohotnikov.ivan@e-queo.net>
- Remove object schema check from CustomObjectFieldTemplate additionalProperties logic
- Remove overflow-y-auto from Sidebar for cleaner scrolling behavior

Signed-off-by: ohotnikov.ivan <ohotnikov.ivan@e-queo.net>
Remove WebSocket proxy config from vite.config and unused labelSelector/fieldSelector parameters from watch() method. These were part of earlier implementation that is no longer needed.

Signed-off-by: ohotnikov.ivan <ohotnikov.ivan@e-queo.net>
Remove Ingresses and Secrets tabs from VMInstance detail page. VMInstance now shows only relevant tabs:
- Overview
- Workloads
- Services
- VNC

Other application types (Kubernetes, PostgreSQL, etc.) continue to show the full tab set including Ingresses and Secrets.

Signed-off-by: ohotnikov.ivan <ohotnikov.ivan@e-queo.net>
Restore labelSelector and fieldSelector parameters in watch() method to enable filtered watch requests. This fixes the issue where watch requests were fetching all resources without label-based filtering, causing unnecessary data to be loaded (e.g., all tenantsecrets instead of only those belonging to a specific application).

Signed-off-by: ohotnikov.ivan <ohotnikov.ivan@e-queo.net>
Fix label-based filtering for Secrets, Services, and Ingresses:

- Add application.group label to appInstanceLabel selector (required by all resources)
- Secrets: Use core Secrets API instead of TenantSecret API, filter by tenantresource=false to show internal application secrets
- Services/Ingresses: Add tenantresource=true filter to show only tenant-facing resources

This matches the filtering logic from the old dashboard and correctly filters resources belonging to specific applications.

Signed-off-by: ohotnikov.ivan <ohotnikov.ivan@e-queo.net>
Simplify appInstanceLabel() by removing redundant application.group filter - kind and name are sufficient to uniquely identify application resources.

Update detail tabs to use simplified label selectors:
- Remove internal.cozystack.io/tenantresource filter
- Switch SecretsTab from core Secrets to TenantSecrets (core.cozystack.io/v1alpha1)
- Update UI text from 'Secrets' to 'Tenant secrets'

Signed-off-by: ohotnikov.ivan <ohotnikov.ivan@e-queo.net>
Remove labelSelector and fieldSelector parameters from watch() method. These were added earlier but are not needed since list() already supports filtering and watch reuses the same resource version.

Signed-off-by: ohotnikov.ivan <ohotnikov.ivan@e-queo.net>
Switch from TenantSecrets CRD to standard v1 Secrets with
internal.cozystack.io/tenantresource=false filter to show
application-specific secrets.

Add collapsible sections for each secret with chevron indicators,
showing secret type and key count in collapsed state.

Fix watch method to properly pass labelSelector and fieldSelector
parameters to Kubernetes API for correct resource filtering.

Signed-off-by: ohotnikov.ivan <ohotnikov.ivan@e-queo.net>
Add eye icon button next to each secret to reveal/hide all keys
at once. Individual key reveal buttons still work independently.

Removed global "Expand all" feature in favor of per-secret reveal
control for better UX.

Signed-off-by: ohotnikov.ivan <ohotnikov.ivan@e-queo.net>
Add flex layout and overflow-y-auto to sidebar nav element to allow
scrolling when menu sections exceed viewport height.

Signed-off-by: ohotnikov.ivan <ohotnikov.ivan@e-queo.net>
Add Edit button to Tenants page for editing Tenant CRs in current
namespace instead of just viewing TenantNamespace list.

Improve free-form object detection in CustomObjectFieldTemplate to
handle additionalProperties with schema objects (not just boolean
true), enabling KeyValueEditor for fields like resourceQuotas.

Signed-off-by: ohotnikov.ivan <ohotnikov.ivan@e-queo.net>
Fix module detection on Tenants page to use TenantModule CRs instead of
namespace labels. Load all TenantModules via K8s API and determine
enabled modules by checking if corresponding TenantModule resources
exist in tenant namespace.

This resolves inverted logic where tenants with all modules enabled
showed none, and vice versa.

Signed-off-by: ohotnikov.ivan <ohotnikov.ivan@e-queo.net>
Add complete backup management section to console UI:
- Backups sidebar section with Plans, Backup Jobs, Backups, and Restore Jobs
- List pages for all backup resource types with Create buttons
- Dynamic form pages with smart selects:
  - Kind selector triggers dynamic instance loading
  - Plan/Backup/BackupClass selects with fallback for empty lists
  - Real-time data fetching from Kubernetes API
- Separate specialized pages for Plans and RestoreJobs with dependent field logic
- Generic pages for BackupJobs and Backups using shared components

Signed-off-by: Aleksei Sviridkin <f@lex.la>
Signed-off-by: ohotnikov.ivan <ohotnikov.ivan@e-queo.net>
Change module detection from TenantModule resources to namespace labels:
- Use namespace.cozystack.io/* labels for module status
- Define MODULE_LABELS mapping with key-label pairs
- Extract enabled modules from namespace labels
- Simplify logic by removing K8s API calls for TenantModules

Signed-off-by: Aleksei Sviridkin <f@lex.la>
Signed-off-by: ohotnikov.ivan <ohotnikov.ivan@e-queo.net>
Change module display logic to use actual TenantModule resources:
- Fetch all TenantModules via K8s API using useK8sList
- Group modules by namespace for each tenant
- Exclude 'info' module from display (default module)
- Show real-time module status from CRD instead of namespace labels

This provides accurate module information synchronized with cluster state.

Signed-off-by: Aleksei Sviridkin <f@lex.la>
Signed-off-by: ohotnikov.ivan <ohotnikov.ivan@e-queo.net>
- Use TenantSecret API for Tenant resources from status.namespace
- Apply tenantresource=true filter for Info and tenant-scoped apps
- Apply tenantresource=false filter for regular applications
- Show all secrets without label filtering for Tenant resources

Signed-off-by: ohotnikov.ivan <ohotnikov.ivan@e-queo.net>
ohotnikov.ivan added 5 commits April 24, 2026 11:55
- Add AdditionalPropertiesField component for fields with additionalProperties schema
  (e.g., users field in bucket with per-user readonly checkbox)
- Auto-detect additionalProperties fields in SchemaForm and apply custom field
- Remove Submit button from all forms using custom ButtonTemplates
- Remove internal.cozystack.io/tenantresource label filter from secrets tab
  to show all application-related secrets

Signed-off-by: ohotnikov.ivan <ohotnikov.ivan@e-queo.net>
…ality

Generate backup resource schemas dynamically from CRD definitions instead
of using hardcoded schemas. Add full CRUD operations for all backup resources.

Changes:
- Add useCRDSchema hook to fetch OpenAPI schemas from CRD definitions
- Update all backup create pages to use CRD schemas
- Enrich schemas with enum values for dropdowns (plans, backups, kinds)
- Add BackupResourceEditPage for editing existing resources
- Add edit and delete buttons to BackupResourceListPage
- Add edit routes for all backup resource types

This ensures schemas are always in sync with CRD definitions and includes
all required fields (applicationRef, strategyRef, takenAt for Backups).

Signed-off-by: ohotnikov.ivan <ohotnikov.ivan@e-queo.net>
…defaults

- Create dedicated BackupCreatePage for Backup resources
- Add default apiGroup value in Plan and RestoreJob forms
- Improve UX with pre-filled apiGroup field

Signed-off-by: ohotnikov.ivan <ohotnikov.ivan@e-queo.net>
- Show only 'enabled' checkbox for disabled addons
- Expand additional fields when addon is enabled
- Add visual grouping for enabled addon fields
- Improve form compactness for apps with many addons (e.g., Kubernetes)

Signed-off-by: ohotnikov.ivan <ohotnikov.ivan@e-queo.net>
Replace the generic "Chart Values" title from Helm schemas with
more user-friendly "Parameters" label in the sanitizeSchema function.

Signed-off-by: ohotnikov.ivan <ohotnikov.ivan@e-queo.net>
Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request implements a comprehensive backup management interface for handling plans, jobs, and restore operations, and introduces a VNC console for VM instances using noVNC. It also enhances the form system with custom RJSF templates and widgets, such as a StorageClass selector and key-value editor, and improves the secrets management UI. Feedback identifies a critical bug in the backup update mutation, recommends a more robust method for deriving resource kinds, and highlights issues with hardcoded WebSocket URLs and state synchronization in the new components.

}

try {
await updateMutation.mutateAsync({ name: name!, resource: updated })
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

The updateMutation.mutateAsync call is passing an object with name and resource properties, but the useK8sUpdate hook's mutationFn expects the resource object directly as its first argument. This will cause the update to fail at runtime because the hook attempts to access body.metadata.name, which will be undefined on the wrapper object.

Suggested change
await updateMutation.mutateAsync({ name: name!, resource: updated })
await updateMutation.mutateAsync(updated)


const resource = {
apiVersion: "backups.cozystack.io/v1alpha1",
kind: title.slice(0, -1), // Remove 's' from plural title
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Deriving the Kubernetes resource kind by slicing the plural title is fragile and incorrect for multi-word resources. For example, if title is "Backup Jobs", the derived kind will be "Backup Job" (with a space), which is not a valid Kubernetes kind (it should be "BackupJob"). It is safer to use an explicit mapping based on the resourceType prop.

Suggested change
kind: title.slice(0, -1), // Remove 's' from plural title
kind: resourceType === "backupjobs" ? "BackupJob" : resourceType === "restorejobs" ? "RestoreJob" : resourceType === "plans" ? "Plan" : "Backup",

let mounted = true

// Build WebSocket URL through kubectl proxy
const wsUrl = `ws://localhost:8001/apis/subresources.kubevirt.io/v1/namespaces/${ns}/virtualmachineinstances/${instance.metadata.name}/vnc`
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The VNC WebSocket URL is hardcoded to localhost:8001, which is only suitable for local development using kubectl proxy. In a production environment, this will fail. Additionally, using the ws:// protocol will be blocked by browsers as mixed content if the console is served over https://. The URL should be constructed dynamically using window.location.host and the appropriate protocol (ws: or wss:).

Comment on lines +15 to +20
const [pairs, setPairs] = useState<KeyValuePair[]>(() => {
return Object.entries(value || {}).map(([key, val]) => ({
key,
value: typeof val === "string" ? val : JSON.stringify(val),
}))
})
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The pairs state is initialized only once when the component mounts. If the value prop changes from the parent (e.g., due to a form reset or switching between different resources), the internal state will not synchronize, leading to stale data being displayed. Consider using a useEffect to sync the state when the value prop changes, or ensuring the parent component uses a unique key to force a re-mount when the underlying data changes.

ohotnikov.ivan added 2 commits April 24, 2026 11:59
Add visual confirmation for options without configuration fields (like upload).
Show selected state with checkmark and informative message in a styled card.
Add helpful hint for upload option about post-creation image upload workflow.

Signed-off-by: ohotnikov.ivan <ohotnikov.ivan@e-queo.net>
Replace SourceWidget with SourceField to properly handle mutually exclusive
source options in VM Disk form. RJSF renders objects with properties as
separate fields when using widgets, but custom fields can override the entire
object rendering.

This fixes the issue where source options (image/http/upload/disk) were
rendered as separate fieldsets instead of radio buttons.

Signed-off-by: ohotnikov.ivan <ohotnikov.ivan@e-queo.net>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 13

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

🟡 Minor comments (25)
apps/console/src/routes/TenantsPage.tsx-35-50 (1)

35-50: ⚠️ Potential issue | 🟡 Minor

Simplify modulesByNamespace — the outer condition is dead code and the inner filter is O(n²).

Line 43's outer check name !== "info" || count > 1 is effectively a no-op: the inner block only pushes when name !== "info", so when name === "info" the inner always skips regardless of the outer count, and when name !== "info" the outer is already true. The net behavior is simply "push every non-info module" — the "info is default, keep it when it's the only one" intent implied by the comment is never realized. Additionally, the .filter(...) inside forEach makes this O(n²) over all modules.

If "info" should always be hidden, drop the dead logic. If "info" should be shown only when it's the sole module in the namespace, precompute counts once.

🛠 Proposed fix (drop dead logic, single pass)
-  // Group modules by namespace
-  const modulesByNamespace = useMemo(() => {
-    const map = new Map<string, string[]>()
-    modulesData?.items.forEach((mod) => {
-      const ns = mod.metadata.namespace
-      if (!map.has(ns)) {
-        map.set(ns, [])
-      }
-      // Only include info module if it's not the only module (info is default)
-      if (mod.metadata.name !== "info" || (modulesData?.items.filter(m => m.metadata.namespace === ns).length ?? 0) > 1) {
-        if (mod.metadata.name !== "info") {
-          map.get(ns)!.push(mod.metadata.name)
-        }
-      }
-    })
-    return map
-  }, [modulesData])
+  // Group modules by namespace (hide the default "info" module)
+  const modulesByNamespace = useMemo(() => {
+    const map = new Map<string, string[]>()
+    modulesData?.items.forEach((mod) => {
+      if (mod.metadata.name === "info") return
+      const ns = mod.metadata.namespace
+      const list = map.get(ns)
+      if (list) list.push(mod.metadata.name)
+      else map.set(ns, [mod.metadata.name])
+    })
+    return map
+  }, [modulesData])
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/console/src/routes/TenantsPage.tsx` around lines 35 - 50,
modulesByNamespace currently performs an O(n²) filter and contains dead logic —
change it to a two-step single-pass approach: first build a Map of namespace ->
count from modulesData?.items (use mod.metadata.namespace) to know how many
modules per namespace, then iterate modulesData?.items once and push
mod.metadata.name into map only for non-"info" modules, and for "info" push it
only when the precomputed count for that namespace is 1; update references to
modulesByNamespace, modulesData?.items, mod.metadata.name and
mod.metadata.namespace accordingly to remove the inner .filter and the redundant
outer condition.
apps/console/src/routes/detail/VncTab.tsx-33-33 (1)

33-33: ⚠️ Potential issue | 🟡 Minor

Simplify the RFB import to use module.default directly.

The @novnc/novnc@1.6.0 package exports RFB as a standard ESM default export (export default class RFB), so module.default should be the RFB class directly. The current fallback chain (module as any).default?.default || module.default || module is overly defensive and the final fallback to module (the entire namespace object) would fail on new RFB(...) with a confusing error. Replace with simply module.default to get the actual RFB constructor, with a proper type annotation instead of any.

This applies to both line 33 and line 131.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/console/src/routes/detail/VncTab.tsx` at line 33, The RFB import
fallback chain is overly defensive and can yield the namespace object instead of
the constructor; replace occurrences where RFB is resolved (the expression
currently using (module as any).default?.default || module.default || module)
with a direct module.default access and add a proper type for the module (use
the RFB constructor type) instead of casting to any; update both places
referenced in VncTab.tsx (the resolution at the original import and the second
occurrence around line 131) so new RFB(...) receives the actual class
constructor.
apps/console/src/routes/detail/SecretsTab.tsx-185-187 (1)

185-187: ⚠️ Potential issue | 🟡 Minor

Import Tenant and narrow the type cast instead of using as any.

The Tenant interface with status.namespace is properly defined in @cozystack/types, making the as any cast unnecessary. Since the code already checks isTenant before accessing the namespace, casting to Tenant provides proper type safety and satisfies the @typescript-eslint/no-explicit-any rule.

🔧 Proposed fix
-import type { ApplicationDefinition, ApplicationInstance } from "@cozystack/types"
+import type { ApplicationDefinition, ApplicationInstance, Tenant } from "@cozystack/types"
  const ns = isTenant
-   ? (instance.status as any)?.namespace ?? instance.metadata.namespace ?? ""
+   ? (instance as Tenant).status?.namespace ?? instance.metadata.namespace ?? ""
    : instance.metadata.namespace ?? ""
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/console/src/routes/detail/SecretsTab.tsx` around lines 185 - 187,
Replace the use of a broad any cast when deriving ns: instead cast instance to
Tenant when isTenant is true and access status.namespace, i.e. narrow the type
instead of using "as any"; update the expression that computes ns (the variables
isTenant, instance.status, instance.metadata and ns) to cast instance to Tenant
in the branch where isTenant is true so TypeScript uses Tenant.status.namespace
safely and the `@typescript-eslint/no-explicit-any` lint error is removed.
apps/console/src/lib/use-crd-schema.ts-1-1 (1)

1-1: ⚠️ Potential issue | 🟡 Minor

Remove unused useQuery import.

ESLint flags this as unused. Since useK8sGet already encapsulates the query, the direct import is not needed.

-import { useQuery } from "@tanstack/react-query"
 import { useK8sGet } from "@cozystack/k8s-client"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/console/src/lib/use-crd-schema.ts` at line 1, Remove the unused import
of useQuery from the top of the file; the file currently imports useQuery but
relies on useK8sGet to encapsulate the query logic, so delete the import line
referencing useQuery to satisfy ESLint and avoid dead code.
apps/console/src/routes/BackupResourceListPage.tsx-117-124 (1)

117-124: ⚠️ Potential issue | 🟡 Minor

Status text falls back to raw "True"/"False" condition status.

When phase is absent, statusText becomes ready.status (literal "True"/"False"/"Unknown"), which is exposed to the user as the status label. That's confusing UX — users see "True" in a badge and have to infer it means "Ready".

♻️ Suggested mapping
-                  const phase = item.status?.phase
-                  const ready = item.status?.conditions?.find((c) => c.type === "Ready")
-                  const statusText = phase || ready?.status || "Unknown"
-                  const statusTone =
-                    statusText === "Completed" || statusText === "True" ? "ok" :
-                    statusText === "Failed" || statusText === "False" ? "error" :
-                    "warn"
+                  const phase = item.status?.phase
+                  const ready = item.status?.conditions?.find((c) => c.type === "Ready")
+                  const readyText =
+                    ready?.status === "True" ? "Ready"
+                    : ready?.status === "False" ? "Not Ready"
+                    : ready?.status === "Unknown" ? "Unknown"
+                    : undefined
+                  const statusText = phase || readyText || "Unknown"
+                  const statusTone =
+                    statusText === "Completed" || statusText === "Ready" ? "ok" :
+                    statusText === "Failed" || statusText === "Not Ready" ? "error" :
+                    "warn"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/console/src/routes/BackupResourceListPage.tsx` around lines 117 - 124,
The UI shows raw condition statuses ("True"/"False") because statusText falls
back to ready.status; change the fallback to a user-facing label by mapping
ready.status values to friendly strings (e.g., "True" -> "Ready", "False" ->
"Not Ready", undefined/other -> "Unknown") where statusText is computed in the
items.map block, and then compute statusTone based on these mapped labels (or
check ready.status booleans first) so badges display "Ready"/"Not
Ready"/"Unknown" instead of raw "True"/"False"; update the logic around phase,
ready, statusText, and statusTone accordingly.
apps/console/src/routes/BackupResourceListPage.tsx-1-2 (1)

1-2: ⚠️ Potential issue | 🟡 Minor

Remove unused useNavigate import and navigate variable.

ESLint flags line 32 as unused. Since no in-component navigation happens here (deletion just refetches), drop both.

-import { Link, useNavigate } from "react-router"
+import { Link } from "react-router"
...
-  const navigate = useNavigate()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/console/src/routes/BackupResourceListPage.tsx` around lines 1 - 2, The
component imports and declares navigation utilities that are unused: remove the
unused import useNavigate and delete the navigate variable/const (from the
BackupResourceListPage component) so ESLint no longer flags it; keep Archive,
Plus, Edit, Trash2 and ensure deletion logic continues to only refetch without
performing any navigation.
apps/console/src/lib/use-crd-schema.ts-41-42 (1)

41-42: ⚠️ Potential issue | 🟡 Minor

Picking versions[0] is not guaranteed to be the served/storage version.

A CRD may declare multiple versions, and the first entry in spec.versions isn’t necessarily the one currently served or marked as storage. Prefer the version where storage === true (or served === true), falling back to versions[0] only if none is explicitly marked.

♻️ Suggested refactor
-  // Extract the schema from the first version's spec field
-  const schema = crd?.spec?.versions?.[0]?.schema?.openAPIV3Schema?.properties?.spec
+  // Prefer the storage version, then any served version, else fall back to the first.
+  const versions = crd?.spec?.versions ?? []
+  const chosen =
+    versions.find((v) => (v as CRDVersion & { storage?: boolean }).storage) ??
+    versions.find((v) => (v as CRDVersion & { served?: boolean }).served) ??
+    versions[0]
+  const schema = chosen?.schema?.openAPIV3Schema?.properties?.spec

You may also want to extend the CRDVersion interface with served?: boolean and storage?: boolean.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/console/src/lib/use-crd-schema.ts` around lines 41 - 42, The current
extraction uses versions[0] which may not be the served/storage version; update
the logic that computes schema to first find the CRD version where storage ===
true (or served === true if storage not present), falling back to versions[0]
only if no matching version is found, then read
thatVersion.schema.openAPIV3Schema.properties.spec; also extend the CRDVersion
type/interface to include optional served?: boolean and storage?: boolean to
accurately type-check the lookup.
apps/console/src/components/AdditionalPropertiesWidget.tsx-15-26 (1)

15-26: ⚠️ Potential issue | 🟡 Minor

Prefer inline validation over alert() for the duplicate-key case.

alert("Key "{newKey}" already exists") is blocking and inconsistent with the rest of the form’s styling. Consider surfacing the duplicate-key state inline (e.g., red border on the input + helper text) and/or disabling the Add button when newKey in (value ?? {}). This also lets keyboard users handle the error without dismissing a modal dialog.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/console/src/components/AdditionalPropertiesWidget.tsx` around lines 15 -
26, Replace the blocking alert in handleAddKey with inline validation: add a
local state (e.g., duplicateKeyError or isDuplicate) that checks newKey against
(value ?? {}) and set that state instead of calling alert; update the input to
show a validation state (red border/helper text) when duplicateKeyError is true
and disable the Add button when newKey is empty or duplicate; keep the rest of
the logic (using itemSchema to compute defaultValue, calling onChange and
setNewKey("")) unchanged so successful adds still work.
apps/console/src/routes/BackupCreatePage.tsx-106-145 (1)

106-145: ⚠️ Potential issue | 🟡 Minor

Replace alert() with inline/toast errors.

alert() is used for all four validation paths and the create-failure branch. This blocks the UI thread, can't be styled or localized, and diverges from the rest of this console app's UX (the same file uses <Section>, <Spinner>, etc. from @cozystack/ui). Prefer setting an error state rendered inline near the relevant field, or use a toast/notification primitive if one exists in @cozystack/ui.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/console/src/routes/BackupCreatePage.tsx` around lines 106 - 145, Replace
all blocking alert() calls in handleSubmit with UI-friendly error reporting: use
local state (e.g., validationError / fieldErrors) to store per-field validation
messages and render them inline next to the corresponding inputs
(applicationRef, strategyRef, takenAt, name) instead of calling alert; for the
create-failure branch use the app’s notification/toast primitive (from
`@cozystack/ui`) or a global error state to show a non-blocking error containing
(err as Error).message; update handleSubmit to set these states where validation
fails and to show a toast on catch instead of alert, and ensure
createMutation.mutateAsync and navigate usage remain unchanged.
apps/console/src/components/KeyValueEditor.tsx-15-20 (1)

15-20: ⚠️ Potential issue | 🟡 Minor

Internal state doesn’t sync when the value prop changes.

The initial pairs list is built only once via the lazy useState initializer. If the parent updates value asynchronously — e.g., the wrapping CustomObjectFieldTemplate re-renders with data fetched from the server on an edit page, or another field’s change causes a schema reset — the editor will keep showing the initial (often empty) pairs. For the new backup edit flows this will look like the overrides silently disappeared.

Either derive pairs directly from value on each render, or add an effect that re-syncs when value changes from outside (e.g., using a shallow equality check to avoid clobbering in-progress edits).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/console/src/components/KeyValueEditor.tsx` around lines 15 - 20, The
component initializes pairs via the lazy useState initializer and never updates
them when the incoming value prop changes; add sync logic so external changes to
value update pairs: either derive pairs from value on each render or add a
useEffect watching value that computes newPairs (map Object.entries(value || {})
to {key, value: typeof val === "string" ? val : JSON.stringify(val)}) and calls
setPairs only when the newPairs differ from the current pairs (use a shallow
equality check on keys/values to avoid clobbering in-progress edits). Refer to
the pairs state and setPairs and the initial useState initializer in
KeyValueEditor to locate where to apply the effect or derivation.
apps/console/src/routes/BackupCreatePage.tsx-129-145 (1)

129-145: ⚠️ Potential issue | 🟡 Minor

Guard submit when tenantNamespace is missing.

On line 134 the resource is built with namespace: tenantNamespace without a null check. If useTenantContext() hasn’t resolved a namespace, this sends namespace: undefined to createMutation.mutateAsync, which will either be rejected by the server or — worse — routed to the default namespace depending on the client. Add an early validation similar to the other alert checks, or disable the submit button while tenantNamespace is falsy.

🛡️ Proposed early check
     if (!formData.takenAt) {
       alert("Taken at timestamp is required")
       return
     }
+
+    if (!tenantNamespace) {
+      alert("No tenant namespace selected")
+      return
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/console/src/routes/BackupCreatePage.tsx` around lines 129 - 145, The
submit builds a resource using namespace: tenantNamespace without checking it;
add an early guard before constructing or calling createMutation.mutateAsync to
prevent sending namespace: undefined—e.g., if tenantNamespace is falsy alert the
user and return (similar to other alert checks) or ensure the submit button is
disabled while tenantNamespace is not set; reference the tenantNamespace
variable and the createMutation.mutateAsync call so you place the guard
immediately before resource construction/await createMutation.mutateAsync.
apps/console/src/components/SchemaForm.tsx-13-73 (1)

13-73: ⚠️ Potential issue | 🟡 Minor

Recursion misses array items and composite schemas.

Both addStorageClassWidgets and addAdditionalPropertiesWidgets only descend into schema.properties. A storageClass field inside an array item (e.g., volumes: { type: "array", items: { properties: { storageClass: {...} } } }) — which is common in CRD-driven schemas — will not pick up the widget, and likewise for schemas composed via allOf/oneOf/anyOf. Given the PR’s goal of applying StorageClassWidget to “all forms with a storageClass field”, this could silently miss real fields.

♻️ Suggested handling for array items (apply similarly to additionalProperties recursion)
   for (const [key, value] of Object.entries(properties)) {
     if (key === "storageClass" && typeof value === "object" && (value as any).type === "string") {
       // Found a storageClass field - add widget
       result[key] = {
         ...result[key],
         "ui:widget": "StorageClassWidget",
       }
     } else if (typeof value === "object" && (value as any).properties) {
       // Recursively process nested objects
       result[key] = addStorageClassWidgets(value as RJSFSchema, result[key] as UiSchema)
+    } else if (
+      typeof value === "object" &&
+      (value as any).type === "array" &&
+      (value as any).items &&
+      typeof (value as any).items === "object"
+    ) {
+      // Recurse into array item schemas
+      const items = (value as any).items
+      result[key] = {
+        ...result[key],
+        items: addStorageClassWidgets(items, (result[key] as UiSchema)?.items as UiSchema),
+      }
     }
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/console/src/components/SchemaForm.tsx` around lines 13 - 73, The current
recursion in addStorageClassWidgets and addAdditionalPropertiesWidgets only
traverses schema.properties and thus misses storageClass inside arrays and
composite schemas; update both functions to also descend into schema.items (for
arrays), schema.additionalProperties (when it is a schema), and each subschema
in schema.allOf/oneOf/anyOf by recursively calling the same function on those
schemas, and merge/attach uiSchema entries appropriately (e.g., propagate
result[key] to its .items or composite index entries) so StorageClassWidget and
AdditionalPropertiesField get applied for nested array items and composed
schemas; modify references inside addStorageClassWidgets and
addAdditionalPropertiesWidgets to detect and recurse into items,
additionalProperties, and allOf/oneOf/anyOf.
apps/console/src/components/StorageClassWidget.tsx-30-34 (1)

30-34: ⚠️ Potential issue | 🟡 Minor

Auto-default useEffect prevents users from clearing the field.

When the field is not required (line 44 renders -- None --), selecting it calls onChange(undefined) (line 39). On the very next render !value is true again, so this effect immediately re-fires and writes the default back — users can never intentionally leave the field empty on clusters that have a default StorageClass.

Use an init ref so the default is applied only once:

🐛 Proposed fix
-import { useEffect } from "react"
+import { useEffect, useRef } from "react"
 ...
 export function StorageClassWidget(props: WidgetProps) {
   const { value, onChange, required, disabled, readonly, placeholder } = props
+  const didInitRef = useRef(false)
   ...
-  // Set default storage class if value is empty and default exists
-  useEffect(() => {
-    if (!value && defaultSC && !isLoading) {
-      onChange(defaultSC.metadata.name)
-    }
-  }, [value, defaultSC, isLoading, onChange])
+  // Set default storage class once on first load if value is empty
+  useEffect(() => {
+    if (didInitRef.current || isLoading) return
+    if (!value && defaultSC) {
+      onChange(defaultSC.metadata.name)
+    }
+    didInitRef.current = true
+  }, [value, defaultSC, isLoading, onChange])

Also, placeholder is destructured on line 16 but never used (matches the ESLint hint) — drop it from the destructure.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/console/src/components/StorageClassWidget.tsx` around lines 30 - 34, The
current useEffect that auto-applies the default StorageClass repeatedly
re-triggers when value is falsy, preventing users from clearing the field;
change the logic in the useEffect in StorageClassWidget to only apply defaultSC
once by introducing an init ref (e.g., let appliedDefaultRef = useRef(false))
and only call onChange(defaultSC.metadata.name) when !appliedDefaultRef.current,
then set appliedDefaultRef.current = true after applying; also remove the unused
placeholder from the props destructure to satisfy ESLint.
apps/console/src/components/MutuallyExclusiveField.tsx-110-117 (1)

110-117: ⚠️ Potential issue | 🟡 Minor

Radio group name collides when title is undefined.

name={${title}-source} becomes "undefined-source" for any instance without a title; two such components on the same page will share a radio group and selecting one clears the other. Fall back to a unique identifier (e.g., a useId() value) instead of interpolating an optional string.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/console/src/components/MutuallyExclusiveField.tsx` around lines 110 -
117, The radio input name currently interpolates title
(name={`${title}-source`}) which yields "undefined-source" when title is missing
and causes distinct MutuallyExclusiveField instances to share a radio group;
update the component (MutuallyExclusiveField) to derive a stable fallback id
using React's useId() and use that id when title is falsy (e.g., const id =
useId(); const groupName = title ? `${title}-source` : `${id}-source`) and
assign groupName to the input name prop so each instance gets a unique radio
group when title is undefined.
apps/console/src/routes/BackupResourceCreatePageWithData.tsx-16-42 (1)

16-42: ⚠️ Potential issue | 🟡 Minor

Enum injection only descends into properties; misses items, oneOf, anyOf, schema-valued additionalProperties.

If a CRD schema wraps the target field in an array, a union, or a $ref-resolved allOf/oneOf branch, the dot-path walk will silently not hit it and the dropdown will render as a plain text input. Worth handling at least items and oneOf/anyOf, or logging a warning when an enumMap key does not resolve so misconfigurations don’t fail silently in the UI.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/console/src/routes/BackupResourceCreatePageWithData.tsx` around lines 16
- 42, The enrichSchemaWithEnums function only traverses .properties so enums
nested under arrays, unions or schema-valued additionalProperties never get
injected; update enrichSchemaWithEnums to also descend into schema slots: check
and recurse into result.items, each branch of
result.oneOf/result.anyOf/result.allOf, and result.additionalProperties when
it's an object, and resolve $ref/allOf branches if your resolver is available;
after traversal, if enumMap contains keys that were not matched, emit a
warning/log with the unresolved path so misconfigurations don’t fail silently.
Ensure you reference enrichSchemaWithEnums and enumMap when adding these extra
recursion cases and the warning.
apps/console/src/routes/BackupPlanCreatePage.tsx-82-112 (1)

82-112: ⚠️ Potential issue | 🟡 Minor

Guard JSON.parse(baseSchema) and validate the enum map paths resolve.

Same pattern as BackupResourceCreatePageWithData.tsx — a malformed baseSchema will throw during render. Additionally, enrichSchemaWithEnums only walks properties, so any mismatch between the dot-paths here (applicationRef.kind, applicationRef.name, backupClassName) and the actual CRD schema shape fails silently. Consider logging when a path in enumMap did not match any node during enrichment to catch schema drift early.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/console/src/routes/BackupPlanCreatePage.tsx` around lines 82 - 112, The
JSON.parse(baseSchema) call inside the useMemo may throw on malformed input and
the enumMap keys may not resolve to nodes that enrichSchemaWithEnums actually
walks, so wrap JSON.parse(baseSchema) in a safe parse (try/catch) and return
null on parse failure; before calling enrichSchemaWithEnums validate that the
dot-paths you set on enumMap (e.g. "applicationRef.kind", "applicationRef.name",
"backupClassName") actually resolve into the parsed base schema (check nested
properties via the same property traversal logic used by enrichSchemaWithEnums
or reuse a helper to resolve paths) and only add entries that resolve; also
modify or augment enrichSchemaWithEnums to emit a log/warning when a requested
enum path cannot be found so schema drift is visible (or capture missing-paths
here and log them via your logger) and finally keep the existing default
assignment on enriched.properties.applicationRef.properties.apiGroup only after
confirming those nested objects exist.
apps/console/src/components/rjsf-templates.tsx-1-14 (1)

1-14: ⚠️ Potential issue | 🟡 Minor

Remove unused imports.

ESLint flags FieldTemplateProps (line 5) and AdditionalPropertiesField (line 14) as unused. AdditionalPropertiesField in particular isn’t registered anywhere in this registry — dead import.

Suggested diff
 import type {
   IconButtonProps,
   TemplatesType,
   WidgetsType,
-  FieldTemplateProps,
   FormContextType,
   RJSFSchema,
   StrictRJSFSchema,
 } from "@rjsf/utils"
 import { CustomObjectFieldTemplate } from "./CustomObjectFieldTemplate.tsx"
 import { SourceWidget } from "./SourceWidget.tsx"
 import { StorageClassWidget } from "./StorageClassWidget.tsx"
 import { AdditionalPropertiesWidget } from "./AdditionalPropertiesWidget.tsx"
-import { AdditionalPropertiesField } from "./AdditionalPropertiesField.tsx"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/console/src/components/rjsf-templates.tsx` around lines 1 - 14, Remove
the dead imports flagged by ESLint: delete the unused FieldTemplateProps import
and the never-registered AdditionalPropertiesField import from the top of the
rjsf templates module; ensure only used symbols (e.g., IconButtonProps,
TemplatesType, WidgetsType, FormContextType, RJSFSchema, StrictRJSFSchema)
remain so imports match actual usage in this file (references:
FieldTemplateProps, AdditionalPropertiesField).
apps/console/src/components/AdditionalPropertiesField.tsx-26-37 (1)

26-37: ⚠️ Potential issue | 🟡 Minor

Trim inconsistency lets users create whitespace-padded duplicate keys.

newKey.trim() gates acceptance on line 27, but the duplicate check on line 28 and the key write on line 35 use the raw newKey. A user entering "foo" followed by " foo " creates two distinct entries. Normalize once and reuse.

🐛 Suggested fix
   const handleAddKey = () => {
-    if (!newKey.trim()) return
-    if (formData && newKey in formData) {
-      alert(`Key "${newKey}" already exists`)
+    const trimmed = newKey.trim()
+    if (!trimmed) return
+    if (formData && trimmed in formData) {
+      alert(`Key "${trimmed}" already exists`)
       return
     }

     // Initialize with default value from schema
     const defaultValue = itemSchema.type === "object" ? {} : itemSchema.default ?? ""
-    onChange({ ...formData, [newKey]: defaultValue })
+    onChange({ ...formData, [trimmed]: defaultValue })
     setNewKey("")
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/console/src/components/AdditionalPropertiesField.tsx` around lines 26 -
37, In handleAddKey normalize newKey once (e.g., const trimmedKey =
newKey.trim()) and use trimmedKey for the empty check, duplicate check (against
formData), the key inserted into onChange, and the alert text; keep
setNewKey("") (or setNewKey(trimmedKey) then clear) to avoid creating
whitespace-padded duplicate keys and ensure consistency when using itemSchema,
formData, onChange and setNewKey.
apps/console/src/routes/BackupResourceCreatePage.tsx-76-127 (1)

76-127: ⚠️ Potential issue | 🟡 Minor

Missing “schema failed to load” fallback — sibling pages render an error, this one renders a blank form.

When schemaLoading is false and schema is still falsy (either crdSchema missing and no overrideSchema), the header + empty Name input render with no indication that the form is broken. BackupPlanCreatePage and BackupRestoreJobCreatePage both handle this with an explicit !schema branch — worth aligning for UX consistency.

   if (schemaLoading) {
     return (
       <div className="flex items-center gap-2 p-8 text-slate-500">
         <Spinner /> Loading schema...
       </div>
     )
   }
+
+  if (!schema) {
+    return (
+      <div className="p-8 text-red-600">
+        Failed to load {title} schema. Please refresh the page.
+      </div>
+    )
+  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/console/src/routes/BackupResourceCreatePage.tsx` around lines 76 - 127,
The page currently shows the header and Name input when schemaLoading is false
but schema is falsy, leaving the form broken; add the same explicit !schema
fallback used in BackupPlanCreatePage and BackupRestoreJobCreatePage so users
see an error state instead of an empty form. After the schemaLoading check in
BackupResourceCreatePage, add a branch that checks if (!schema) and returns the
same error UI (message + possible retry/navigation control) used by the sibling
pages, ensuring SchemaForm (openAPISchema prop) and the rest of the form are not
rendered when schema is missing; reference the schemaLoading and schema
variables and the SchemaForm/handleSubmit components to locate where to insert
this fallback.
apps/console/src/routes/BackupResourceCreatePageWithData.tsx-86-139 (1)

86-139: ⚠️ Potential issue | 🟡 Minor

JSON.parse(baseSchema) can throw inside useMemo.

If useCRDSchema ever returns a non-JSON string (e.g., an error payload, partial response), the throw propagates out of render and there is no user-visible fallback. Wrap in try/catch and render the same error state as !schema.

🛡️ Suggested guard
   const enrichedSchema = useMemo(() => {
     if (!baseSchema) return null

-    const base = JSON.parse(baseSchema)
+    let base: any
+    try {
+      base = JSON.parse(baseSchema)
+    } catch (err) {
+      console.error("Failed to parse CRD schema", err)
+      return null
+    }
     const enumMap: Record<string, string[]> = {}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/console/src/routes/BackupResourceCreatePageWithData.tsx` around lines 86
- 139, The JSON.parse(baseSchema) call inside the useMemo that computes
enrichedSchema can throw and crash rendering; wrap the parse/enrichment logic in
a try/catch within the useMemo (the function where enrichedSchema is computed)
and on error return null (same as the !schema case) so the component renders the
existing error/fallback UI; ensure you still compute enumMap and call
enrichSchemaWithEnums only when parsing succeeds, and keep the same dependency
list ([baseSchema, resourceType, appDefs, plansData, backupsData,
backupClassesData]).
apps/console/src/components/AdditionalPropertiesEditor.tsx-33-33 (1)

33-33: ⚠️ Potential issue | 🟡 Minor

Avoid alert() for duplicate-key feedback.

Blocking alert() is inconsistent with the rest of the console UI. Prefer inline validation text next to the input (e.g., a red helper label) or a toast. This also improves a11y.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/console/src/components/AdditionalPropertiesEditor.tsx` at line 33,
Replace the blocking alert call used for duplicate keys in
AdditionalPropertiesEditor (the code path using newKey) with non-blocking UI
feedback: update the component's state (e.g., set a validation error like
setKeyError or inlineValidation) so an inline red helper label appears beside
the input, or trigger the app's toast mechanism (e.g., call showToast or
setToast) instead of alert; adjust the add-property handler (handleAddProperty /
onAddProperty) to check the existing properties list and set the validation
state or toast when a duplicate is detected, and ensure the input field uses
that validation state to render the helper text and aria-describedby for
accessibility.
apps/console/src/routes/BackupResourceEditPage.tsx-79-79 (1)

79-79: ⚠️ Potential issue | 🟡 Minor

Replace alert() with the app's toast/notification primitive.

Native alert() is blocking, not styled, and inconsistent with the rest of the console UI. Use the shared toast component for error feedback, and ideally surface the error inline near the Save button as well.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/console/src/routes/BackupResourceEditPage.tsx` at line 79, Replace the
blocking native alert in BackupResourceEditPage (the alert(`Failed to update
${title.slice(0, -1)}: ${(err as Error).message}`) call) with the app's shared
toast/notification primitive: import and call the toast API used across the
console (e.g., toast.error(...) or showToast({type: 'error', title: 'Update
failed', message: ...})) and pass the same formatted message; additionally set a
local component state like saveError and update it with the error message so the
UI can render an inline error message near the Save button (clear saveError when
retrying or on success). Ensure you remove the alert() call and use the toast
API plus the saveError state in the Save handler in BackupResourceEditPage.
apps/console/src/components/AdditionalPropertiesEditor.tsx-30-41 (1)

30-41: ⚠️ Potential issue | 🟡 Minor

Trimmed for validation, stored raw — keys with whitespace slip through.

handleAddKey validates !newKey.trim() and checks duplicates against the raw newKey, but stores newKey verbatim on line 39. A user entering " foo " creates a key " foo " that won't collide with an existing "foo" and displays with stray whitespace. Normalize once and reuse.

🔧 Suggested fix
   const handleAddKey = () => {
-    if (!newKey.trim()) return
-    if (value && newKey in value) {
-      alert(`Key "${newKey}" already exists`)
+    const key = newKey.trim()
+    if (!key) return
+    if (value && key in value) {
+      alert(`Key "${key}" already exists`)
       return
     }
 
     // Initialize with default value from schema
     const defaultValue = itemSchema.type === "object" ? {} : itemSchema.default ?? ""
-    onChange({ ...value, [newKey]: defaultValue })
+    onChange({ ...value, [key]: defaultValue })
     setNewKey("")
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/console/src/components/AdditionalPropertiesEditor.tsx` around lines 30 -
41, handleAddKey uses newKey.trim() for validation but then stores the raw
newKey, allowing keys with surrounding whitespace; fix by normalizing newKey
once at start (e.g., const normalizedKey = newKey.trim()), use normalizedKey for
the duplicate check (against value), use normalizedKey when adding the property
in onChange (with defaultValue computed from itemSchema), and then clear the
input with setNewKey(""); update all references in handleAddKey to the
normalizedKey so whitespace can't create distinct keys.
apps/console/src/routes/BackupResourceEditPage.tsx-65-65 (1)

65-65: ⚠️ Potential issue | 🟡 Minor

Import FormEvent as a named import instead of using React.FormEvent.

The file uses React.FormEvent without importing the React namespace. With the modern JSX transform ("jsx": "react-jsx"), the React namespace is not automatically in scope. Only useEffect and useState are imported from react. Update to use the named import:

🔧 Proposed fix
-import { useEffect, useState } from "react"
+import { useEffect, useState, type FormEvent } from "react"
@@
-  const handleSubmit = async (e: React.FormEvent) => {
+  const handleSubmit = async (e: FormEvent) => {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/console/src/routes/BackupResourceEditPage.tsx` at line 65, The handler
signature uses React.FormEvent but the React namespace isn't imported under the
modern JSX transform; update the import from 'react' to include the named type
FormEvent (alongside existing useEffect/useState), and change the function
signature to use FormEvent (e.g., handleSubmit = async (e: FormEvent) => { ...
}) so the type resolves correctly; locate the handleSubmit function and the
top-level import that currently imports useEffect/useState and add FormEvent
there.
apps/console/src/components/AdditionalPropertiesEditor.tsx-38-38 (1)

38-38: ⚠️ Potential issue | 🟡 Minor

Handle type arrays in default value initialization.

RJSFSchema.type supports string | string[] per JSON Schema Draft 7 spec (e.g., ["object", "null"]). The current check itemSchema.type === "object" only works for string types and fails for arrays, defaulting to "" which may be invalid for object types.

Normalize the type check:

Suggested fix
-    const defaultValue = itemSchema.type === "object" ? {} : itemSchema.default ?? ""
+    const types = Array.isArray(itemSchema.type) ? itemSchema.type : [itemSchema.type]
+    const defaultValue =
+      itemSchema.default ??
+      (types.includes("object") ? {} : types.includes("array") ? [] : "")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/console/src/components/AdditionalPropertiesEditor.tsx` at line 38, The
defaultValue initialization incorrectly treats itemSchema.type as a string only;
update the check so it handles string or string[] (per RJSFSchema.type) by
detecting arrays and testing includes('object') — e.g., replace the single
equality check with a boolean like (Array.isArray(itemSchema.type) ?
itemSchema.type.includes("object") : itemSchema.type === "object") when deciding
to use {} versus itemSchema.default ?? "". Ensure you keep the existing fallback
to itemSchema.default and then "" as before; reference variable defaultValue and
the itemSchema.type property.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5d22fe6b-1578-483e-8cac-0589d8d1adce

📥 Commits

Reviewing files that changed from the base of the PR and between 2b197ef and 7ad2875.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (31)
  • apps/console/package.json
  • apps/console/src/components/AdditionalPropertiesEditor.tsx
  • apps/console/src/components/AdditionalPropertiesField.tsx
  • apps/console/src/components/AdditionalPropertiesWidget.tsx
  • apps/console/src/components/CustomObjectFieldTemplate.tsx
  • apps/console/src/components/KeyValueEditor.tsx
  • apps/console/src/components/MutuallyExclusiveField.tsx
  • apps/console/src/components/SchemaForm.tsx
  • apps/console/src/components/SourceWidget.tsx
  • apps/console/src/components/StorageClassWidget.tsx
  • apps/console/src/components/rjsf-templates.tsx
  • apps/console/src/components/schema-form.css
  • apps/console/src/lib/keys-order.ts
  • apps/console/src/lib/use-crd-schema.ts
  • apps/console/src/routes/BackupCreatePage.tsx
  • apps/console/src/routes/BackupPlanCreatePage.tsx
  • apps/console/src/routes/BackupResourceCreatePage.tsx
  • apps/console/src/routes/BackupResourceCreatePageWithData.tsx
  • apps/console/src/routes/BackupResourceEditPage.tsx
  • apps/console/src/routes/BackupResourceListPage.tsx
  • apps/console/src/routes/BackupRestoreJobCreatePage.tsx
  • apps/console/src/routes/ConsolePage.tsx
  • apps/console/src/routes/TenantsPage.tsx
  • apps/console/src/routes/backup-schemas.ts
  • apps/console/src/routes/detail/ApplicationDetailPage.tsx
  • apps/console/src/routes/detail/SecretsTab.tsx
  • apps/console/src/routes/detail/VncTab.tsx
  • apps/console/src/routes/sidebar-sections.tsx
  • packages/k8s-client/src/client.ts
  • packages/k8s-client/src/hooks.ts
  • packages/ui/src/components/layout/Sidebar.tsx

Comment on lines +14 to +41
const { schema, formData, onChange, readonly, disabled } = props

// Check if this is a free-form key-value object
// ONLY use KeyValueEditor for truly free-form objects where both keys and values are arbitrary
// This means x-kubernetes-preserve-unknown-fields OR additionalProperties: true (boolean, not a schema object)
const isFreeFormObject =
(!schema.properties || Object.keys(schema.properties).length === 0) &&
((schema as any)["x-kubernetes-preserve-unknown-fields"] === true ||
schema.additionalProperties === true)

// If it's a free-form key-value object, use our custom editor
if (isFreeFormObject) {
return (
<div className="field">
{props.title && (
<label className="control-label">
{props.title}
{props.required && <span className="required">*</span>}
</label>
)}
{props.description && <p className="field-description">{props.description}</p>}
<KeyValueEditor
value={formData || {}}
onChange={onChange}
readonly={readonly || disabled}
/>
</div>
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

In @rjsf/utils v5.24, does the ObjectFieldTemplateProps interface include an onChange prop, or is onChange only exposed on FieldProps and WidgetProps?

💡 Result:

In @rjsf/utils v5.24, the ObjectFieldTemplateProps interface does not include an onChange prop. It includes props such as id, classNames, title, description, displayTitle, properties, required, readonly, disabled, registry, formContext, schema, uiSchema, and formData. Specific callbacks like onAddProperty (in later versions) or onAddClick (in v5) are present for adding properties, but no general onChange prop. The onChange prop is explicitly part of FieldProps and WidgetProps interfaces. FieldProps includes onChange: (newValue: T | undefined, path: FieldPathList, es?: ErrorSchema, id?: string) => void. WidgetProps includes onChange: (value: any, es?: ErrorSchema, id?: string) => void. ObjectFieldTemplate is a layout template for rendering object properties; it receives pre-rendered property content via the properties array and does not handle value changes directly. Change handling is managed by the underlying Field and Widget components.

Citations:


🏁 Script executed:

fd CustomObjectFieldTemplate.tsx

Repository: cozystack/cozystack-ui

Length of output: 124


🏁 Script executed:

cat -n apps/console/src/components/CustomObjectFieldTemplate.tsx | head -50

Repository: cozystack/cozystack-ui

Length of output: 2280


🏁 Script executed:

cat -n apps/console/src/components/CustomObjectFieldTemplate.tsx

Repository: cozystack/cozystack-ui

Length of output: 4168


🏁 Script executed:

cat -n apps/console/src/components/KeyValueEditor.tsx

Repository: cozystack/cozystack-ui

Length of output: 4019


onChange is not a prop of ObjectFieldTemplateProps — KeyValueEditor will throw a runtime error.

Per the RJSF v5 @rjsf/utils types, ObjectFieldTemplateProps does not expose onChange. Destructuring it on line 14 yields undefined, so when KeyValueEditor calls onChange(obj) on line 34 of its implementation, it throws TypeError: onChange is not a function. This completely breaks the free-form object editor (x-kubernetes-preserve-unknown-fields / additionalProperties: true).

The canonical way to propagate value changes from an ObjectFieldTemplate is through per-property props.properties[i].content children (lines 65, 73, 92 do this correctly). For a whole-object custom editor, register it as a Field (RegistryFieldsType) with FieldProps (which has onChange), not as an ObjectFieldTemplate.

Suggested fix

Option A (preferred): Extract the free-form branch into a dedicated Field registered under uiSchema["ui:field"], which receives a real onChange: FieldProps["onChange"].

Option B (quick): Replace isFreeFormObject branch with props.properties.map((prop) => prop.content) (like the default fallback), avoiding custom change handling entirely.

🧰 Tools
🪛 ESLint

[error] 21-21: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/console/src/components/CustomObjectFieldTemplate.tsx` around lines 14 -
41, The free-form branch in CustomObjectFieldTemplate uses a nonexistent
onChange from ObjectFieldTemplateProps causing runtime errors when
KeyValueEditor calls onChange; fix by extracting that branch into a proper Field
(register under uiSchema["ui:field"]) so the component receives FieldProps
including a real onChange, or alternatively revert the free-form branch to
render props.properties.map(p => p.content) (matching the default
ObjectFieldTemplate) to avoid custom change handling; reference
ObjectFieldTemplateProps, KeyValueEditor, props.properties, FieldProps, and
uiSchema["ui:field"] when implementing the change.

Comment on lines +4 to +15
interface MutuallyExclusiveFieldProps {
schema: RJSFSchema
formData: Record<string, unknown> | undefined
handleChange: (data: Record<string, unknown> | undefined) => void
title?: string
description?: string
required?: boolean
}

export function MutuallyExclusiveField(props: MutuallyExclusiveFieldProps) {
const { schema, formData, onChange, title, description, required } = props
const handleChange = onChange
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Interface declares handleChange, component destructures onChange — every interaction throws.

MutuallyExclusiveFieldProps on line 7 exposes handleChange, but line 14 destructures onChange from props. That yields undefined; line 15 then aliases handleChange = undefined, and the first call (handleSelect / handleClear / the nested <input> onChange) crashes with TypeError: handleChange is not a function. The component is non-functional as written — unit-test-level bug.

🐛 Suggested fix
 export function MutuallyExclusiveField(props: MutuallyExclusiveFieldProps) {
-  const { schema, formData, onChange, title, description, required } = props
-  const handleChange = onChange
+  const { schema, formData, handleChange, title, description, required } = props
   const properties = schema.properties || {}

Also worth double-checking the caller side — the AI summary notes SchemaForm’s new wiring does not currently target MutuallyExclusiveField, so this may be dead on the main path today. Either wire it in and fix the prop, or drop the file until it’s needed.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/console/src/components/MutuallyExclusiveField.tsx` around lines 4 - 15,
The component MutuallyExclusiveField declares a prop named handleChange in
MutuallyExclusiveFieldProps but destructures onChange from props, causing
handleChange to be undefined and subsequent calls to fail; fix by making the
prop name consistent — either rename the interface member to onChange or
destructure handleChange from props in the MutuallyExclusiveField function, and
ensure the internal alias (currently const handleChange = onChange) uses the
correct identifier; also verify callers provide the matching prop name
(handleChange or onChange) to MutuallyExclusiveField to keep wiring consistent
with SchemaForm.

Comment on lines +16 to +31
function IconButton<
T = any,
S extends StrictRJSFSchema = RJSFSchema,
F extends FormContextType = any,
>(props: IconButtonProps<T, S, F>) {
const { icon, iconType, uiSchema, registry, className, ...btnProps } = props
return (
<button
type="button"
className={className}
{...btnProps}
>
{icon}
</button>
)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's see the full file to understand the context
head -100 apps/console/src/components/rjsf-templates.tsx

Repository: cozystack/cozystack-ui

Length of output: 2080


🏁 Script executed:

# Check IconButtonProps definition to see if it includes a type property
rg "IconButtonProps" --type tsx --type ts -A 5

Repository: cozystack/cozystack-ui

Length of output: 94


🏁 Script executed:

# Search for IconButtonProps without type restriction
rg "IconButtonProps" -A 5 apps/console/src/components/rjsf-templates.tsx

Repository: cozystack/cozystack-ui

Length of output: 362


🏁 Script executed:

# Check if FieldTemplateProps is used anywhere in the file
rg "FieldTemplateProps" apps/console/src/components/rjsf-templates.tsx

# Check if AdditionalPropertiesField is used anywhere in the file
rg "AdditionalPropertiesField" apps/console/src/components/rjsf-templates.tsx

Repository: cozystack/cozystack-ui

Length of output: 164


🏁 Script executed:

# Get the full file to see if there's any usage of these imports
wc -l apps/console/src/components/rjsf-templates.tsx

# Get full content
cat -n apps/console/src/components/rjsf-templates.tsx

Repository: cozystack/cozystack-ui

Length of output: 2578


🏁 Script executed:

# Search for usage of customTemplates in the codebase
rg "customTemplates" apps/console/src/ -n

# Also search for RJSF Form usage to understand the context
rg "Form" apps/console/src/components/ -n | head -20

Repository: cozystack/cozystack-ui

Length of output: 2848


🏁 Script executed:

# Check SchemaForm to see how Form is used and how onSubmit is handled
sed -n '120,160p' apps/console/src/components/SchemaForm.tsx

# Also check AdditionalPropertiesField for context on submit
sed -n '15,30p' apps/console/src/components/AdditionalPropertiesField.tsx

Repository: cozystack/cozystack-ui

Length of output: 1258


🏁 Script executed:

# Check all RJSF Form usages to see if any don't override SubmitButton
rg "Form" apps/console/src/components/ -B 5 -A 15 | grep -A 20 "templates=" | head -80

Repository: cozystack/cozystack-ui

Length of output: 6574


🏁 Script executed:

# Verify that IconButtonProps from `@rjsf/utils` doesn't define a type property
# Let's check if there's any node_modules or package info
fd "package.json" apps/console/ -x cat {}

# Also check the actual usage pattern to confirm all buttons should be type="button" except SubmitButton
rg "ButtonTemplates" apps/console/src/components/ -A 20 | head -60

Repository: cozystack/cozystack-ui

Length of output: 5457


SubmitButton hardcoded to type="button" creates a footgun for form submission.

IconButton always sets type="button", so when used for SubmitButton (lines 54-56), it breaks native form submit semantics. Currently, all forms in the codebase mitigate this by explicitly overriding SubmitButton to null or hiding it via uiSchema, but this is a brittle pattern that invites bugs when the templates are reused. The other button types (Add, Remove, MoveUp, MoveDown) correctly need type="button", but SubmitButton should be type="submit".

♻️ Suggested fix
-function IconButton<
+function IconButton<
   T = any,
   S extends StrictRJSFSchema = RJSFSchema,
   F extends FormContextType = any,
->(props: IconButtonProps<T, S, F>) {
-  const { icon, iconType, uiSchema, registry, className, ...btnProps } = props
+>(props: IconButtonProps<T, S, F> & { type?: "button" | "submit" }) {
+  const { icon, iconType, uiSchema, registry, className, type = "button", ...btnProps } = props
   return (
     <button
-      type="button"
+      type={type}
       className={className}
       {...btnProps}
     >
       {icon}
     </button>
   )
 }
@@
-    SubmitButton: (props) => (
-      <IconButton {...props} icon="Submit" className={buttonClassName} />
-    ),
+    SubmitButton: (props) => (
+      <IconButton {...props} type="submit" icon="Submit" className={buttonClassName} />
+    ),

Also applies to: 54-56

Remove unused imports: FieldTemplateProps (line 5) and AdditionalPropertiesField (line 14).

🧰 Tools
🪛 ESLint

[error] 16-16: Fast refresh only works when a file only exports components. Move your component(s) to a separate file. If all exports are HOCs, add them to the extraHOCs option.

(react-refresh/only-export-components)


[error] 17-17: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


[error] 19-19: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


[error] 21-21: 'iconType' is assigned a value but never used.

(@typescript-eslint/no-unused-vars)


[error] 21-21: 'uiSchema' is assigned a value but never used.

(@typescript-eslint/no-unused-vars)


[error] 21-21: 'registry' is assigned a value but never used.

(@typescript-eslint/no-unused-vars)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/console/src/components/rjsf-templates.tsx` around lines 16 - 31,
IconButton currently hardcodes type="button", which breaks SubmitButton; update
IconButton (used by SubmitButton, AddButton, RemoveButton, MoveUpButton,
MoveDownButton) to not force the button type—either accept and spread an
explicit type prop from IconButtonProps or default to leaving type undefined so
callers can set type="submit" for SubmitButton and type="button" for control
buttons; then change the SubmitButton usage to supply type="submit" (or ensure
the SubmitButton component passes type="submit" to IconButton). Also remove the
unused imports FieldTemplateProps and AdditionalPropertiesField from this file.

Comment on lines +10 to +14
const currentOption = value
? options.find((opt: string) => value[opt] !== undefined && value[opt] !== null)
: undefined

const [selected, setSelected] = useState<string | undefined>(currentOption)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Selected state won't re-sync when value changes externally.

useState(currentOption) captures only the initial currentOption. If the form data loads asynchronously (common in edit flows where value is undefined on first render and populated later), or is reset by the parent, the radio selection will be stuck on the initial value and diverge from value.

Prefer deriving selected directly from value (single source of truth), or sync with useEffect:

♻️ Proposed fix (derive, don't cache)
-  const currentOption = value
-    ? options.find((opt: string) => value[opt] !== undefined && value[opt] !== null)
-    : undefined
-
-  const [selected, setSelected] = useState<string | undefined>(currentOption)
+  const selected = value
+    ? options.find((opt: string) => value[opt] !== undefined && value[opt] !== null)
+    : undefined

Then drop setSelected calls from handleSelect/handleClear — the onChange already drives the value, which drives selected.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/console/src/components/SourceWidget.tsx` around lines 10 - 14, The local
selected state is initialized from currentOption and won't update when value
changes externally; replace it by deriving selected from value (compute
currentOption each render) or add a useEffect that setsSelected(currentOption)
whenever value or options change, and remove direct setSelected calls from
handleSelect/handleClear so the component's radio selection is driven solely by
the incoming value; reference currentOption, selected, setSelected,
handleSelect, handleClear, value, and options when making the change.

Comment on lines +100 to +116
<label className="flex items-start gap-2 cursor-pointer">
<input
type="radio"
name={`${id}-source`}
checked={isSelected}
onChange={() => handleSelect(option)}
className="mt-0.5 size-4 rounded-full border-slate-300 text-blue-600 focus:ring-blue-500"
/>
<div className="flex-1">
<div className="text-sm font-medium text-slate-900">{option}</div>
{optionDescription && (
<div className="text-xs text-slate-500 mt-0.5">{optionDescription}</div>
)}
{isSelected && renderFieldInput(option)}
</div>
</label>
</div>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Nested form inputs inside a <label> cause click hijacking & a11y issues.

renderFieldInput(option) renders <input type="text"> children at line 113, but they are descendants of the <label> that wraps the radio (line 100). Clicking any sub-field input will also toggle/activate the associated radio and may steal focus, and screen readers will read nested controls as part of the radio's accessible name.

Close the <label> around just the radio + heading, and render renderFieldInput(option) as a sibling:

♻️ Proposed structure
-              <label className="flex items-start gap-2 cursor-pointer">
+              <label className="flex items-start gap-2 cursor-pointer">
                 <input
                   type="radio"
                   name={`${id}-source`}
                   checked={isSelected}
                   onChange={() => handleSelect(option)}
                   className="mt-0.5 size-4 rounded-full border-slate-300 text-blue-600 focus:ring-blue-500"
                 />
                 <div className="flex-1">
                   <div className="text-sm font-medium text-slate-900">{option}</div>
                   {optionDescription && (
                     <div className="text-xs text-slate-500 mt-0.5">{optionDescription}</div>
                   )}
-                  {isSelected && renderFieldInput(option)}
                 </div>
-              </label>
+              </label>
+              {isSelected && renderFieldInput(option)}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<label className="flex items-start gap-2 cursor-pointer">
<input
type="radio"
name={`${id}-source`}
checked={isSelected}
onChange={() => handleSelect(option)}
className="mt-0.5 size-4 rounded-full border-slate-300 text-blue-600 focus:ring-blue-500"
/>
<div className="flex-1">
<div className="text-sm font-medium text-slate-900">{option}</div>
{optionDescription && (
<div className="text-xs text-slate-500 mt-0.5">{optionDescription}</div>
)}
{isSelected && renderFieldInput(option)}
</div>
</label>
</div>
<label className="flex items-start gap-2 cursor-pointer">
<input
type="radio"
name={`${id}-source`}
checked={isSelected}
onChange={() => handleSelect(option)}
className="mt-0.5 size-4 rounded-full border-slate-300 text-blue-600 focus:ring-blue-500"
/>
<div className="flex-1">
<div className="text-sm font-medium text-slate-900">{option}</div>
{optionDescription && (
<div className="text-xs text-slate-500 mt-0.5">{optionDescription}</div>
)}
</div>
</label>
{isSelected && renderFieldInput(option)}
</div>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/console/src/components/SourceWidget.tsx` around lines 100 - 116, The
radio's <label> currently wraps the entire option including nested inputs
returned by renderFieldInput(option), causing click hijacking and a11y issues;
update the JSX so the <label> (containing the <input type="radio">, the option
title text and optional optionDescription) is closed before rendering
renderFieldInput(option) and then render renderFieldInput(option) as a sibling
element (outside the label) only when isSelected is true; ensure handleSelect
remains on the radio's onChange and that styles/layout remain unchanged while
removing nested form controls from inside the label.

Comment on lines +59 to +63
useEffect(() => {
if (resource?.spec) {
setFormData(resource.spec)
}
}, [resource])
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

User edits will be overwritten on any resource refetch.

This effect reassigns formData whenever the resource reference changes (e.g., React Query refetch on window focus, interval polling, cache invalidation after sibling mutations). If the user is mid-edit, their unsaved changes silently disappear. Initialize once (e.g., keyed on resource.metadata.resourceVersion or only when formData is empty), or seed state lazily via useState once the resource is present.

🔧 Example fix: initialize only when resourceVersion changes
-  const [formData, setFormData] = useState<any>({})
+  const [formData, setFormData] = useState<any>({})
+  const initializedVersionRef = useRef<string | undefined>(undefined)
@@
-  useEffect(() => {
-    if (resource?.spec) {
-      setFormData(resource.spec)
-    }
-  }, [resource])
+  useEffect(() => {
+    const rv = resource?.metadata?.resourceVersion
+    if (resource?.spec && rv !== initializedVersionRef.current) {
+      initializedVersionRef.current = rv
+      setFormData(resource.spec)
+    }
+  }, [resource])
🧰 Tools
🪛 ESLint

[error] 61-61: Error: Calling setState synchronously within an effect can trigger cascading renders

Effects are intended to synchronize state between React and external systems such as manually updating the DOM, state management libraries, or other platform APIs. In general, the body of an effect should do one or both of the following:

  • Update external systems with the latest state from React.
  • Subscribe for updates from some external system, calling setState in a callback function when external state changes.

Calling setState synchronously within an effect body causes cascading renders that can hurt performance, and is not recommended. (https://react.dev/learn/you-might-not-need-an-effect).

/home/jailuser/git/apps/console/src/routes/BackupResourceEditPage.tsx:61:7
59 | useEffect(() => {
60 | if (resource?.spec) {

61 | setFormData(resource.spec)
| ^^^^^^^^^^^ Avoid calling setState() directly within an effect
62 | }
63 | }, [resource])
64 |

(react-hooks/set-state-in-effect)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/console/src/routes/BackupResourceEditPage.tsx` around lines 59 - 63, The
effect currently overwrites formData on every resource refetch; change
initialization so formData is only set when appropriate (e.g., on first load or
when the resource version changes) by modifying the useEffect that calls
setFormData(resource.spec). Update the effect to guard with a check such as only
set when formData is empty or when resource.metadata.resourceVersion has changed
(instead of reacting to resource reference changes), so user edits in-flight are
not clobbered; keep the same identifiers (useEffect, setFormData, formData,
resource.spec, resource.metadata.resourceVersion) to locate and update the
logic.

Comment on lines 50 to 53
const { data } = useK8sGet<K8sResource<unknown, unknown> & SecretLike>(
{ ...TENANT_SECRETS_REF, namespace, name },
{ enabled: revealed },
{ ...SECRETS_REF, namespace, name },
{ enabled: shouldReveal },
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm the shape returned by the tenantsecrets API vs v1/secrets
rg -nP -C3 'tenantsecrets|TenantSecret' --type=ts
rg -nP -C3 'useK8sGet\s*<' apps/console/src/routes/detail/SecretsTab.tsx

Repository: cozystack/cozystack-ui

Length of output: 1548


🏁 Script executed:

# Check SecretRow function signature and props
sed -n '35,65p' apps/console/src/routes/detail/SecretsTab.tsx

Repository: cozystack/cozystack-ui

Length of output: 993


🏁 Script executed:

# Check SecretItem and how it calls SecretRow
sed -n '145,175p' apps/console/src/routes/detail/SecretsTab.tsx

Repository: cozystack/cozystack-ui

Length of output: 981


🏁 Script executed:

# Check the full SecretsTab function to see how it passes to SecretItem
sed -n '175,210p' apps/console/src/routes/detail/SecretsTab.tsx

Repository: cozystack/cozystack-ui

Length of output: 1175


🏁 Script executed:

# Find SECRETS_REF definition
rg -n 'SECRETS_REF\s*=' apps/console/src/routes/detail/SecretsTab.tsx

Repository: cozystack/cozystack-ui

Length of output: 91


🏁 Script executed:

# Read SECRETS_REF definition
sed -n '13,18p' apps/console/src/routes/detail/SecretsTab.tsx

Repository: cozystack/cozystack-ui

Length of output: 148


Per-key reveal fetch always targets v1/secrets, even when listing tenantsecrets.

SecretRow hardcodes SECRETS_REF (core v1/secrets) for its useK8sGet, but SecretsTab may have listed items from core.cozystack.io/v1alpha1/tenantsecrets. For the Tenant branch this GET hits a different API than the one that produced the list; it will silently 404 or (if a same-named Secret exists in the namespace) fetch a different object than intended. The UI partially hides this because fullValue falls back to decodeValue(base64Value), so the only symptom may be an unused failing network request — but it will also surface the wrong object if names collide.

Pass the effective apiRef down from SecretsTab instead of hardcoding:

🔧 Proposed fix
 function SecretRow({
   namespace,
   name,
   keyName,
   base64Value,
   forceReveal,
+  apiRef,
 }: {
   namespace: string
   name: string
   keyName: string
   base64Value: string
   forceReveal?: boolean
+  apiRef: { apiGroup: string; apiVersion: string; plural: string }
 }) {
   ...
   const { data } = useK8sGet<K8sResource<unknown, unknown> & SecretLike>(
-    { ...SECRETS_REF, namespace, name },
+    { ...apiRef, namespace, name },
     { enabled: shouldReveal },
   )

…and thread apiRef through SecretItemSecretRow.

Alternatively, since the list response already contains secret.data, drop the per-row useK8sGet entirely and rely on the decoded base64Value.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/console/src/routes/detail/SecretsTab.tsx` around lines 50 - 53,
SecretRow currently hardcodes SECRETS_REF when calling useK8sGet which causes
per-row GETs to always target core/v1 secrets even when the list in SecretsTab
used another apiRef (e.g., core.cozystack.io/v1alpha1/tenantsecrets); update
SecretsTab to pass the effective apiRef down through SecretItem into SecretRow
and change the useK8sGet call in SecretRow to use that passed apiRef instead of
SECRETS_REF (or remove the per-row useK8sGet entirely and use the
already-present list data.secret.data/base64Value to compute fullValue),
ensuring the GET targets the same API that produced the list.

let mounted = true

// Build WebSocket URL through kubectl proxy
const wsUrl = `ws://localhost:8001/apis/subresources.kubevirt.io/v1/namespaces/${ns}/virtualmachineinstances/${instance.metadata.name}/vnc`
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Hardcoded ws://localhost:8001/... will only work behind a local kubectl proxy.

This endpoint is pinned to ws://localhost:8001, so the VNC tab will silently fail for any deployment that isn't sitting behind kubectl proxy on the developer's machine. On an HTTPS-served console the browser will also block ws:// due to mixed content. Since this is user-facing behavior (not just a WIP stub), it should at least:

  1. Derive the scheme from window.location.protocol (wss: on HTTPS pages).
  2. Derive the host from the current origin, or route through an app-defined proxy path, so the same build works in dev and prod.
🔧 Sketch
-    const wsUrl = `ws://localhost:8001/apis/subresources.kubevirt.io/v1/namespaces/${ns}/virtualmachineinstances/${instance.metadata.name}/vnc`
+    const scheme = window.location.protocol === "https:" ? "wss" : "ws"
+    const wsUrl = `${scheme}://${window.location.host}/apis/subresources.kubevirt.io/v1/namespaces/${ns}/virtualmachineinstances/${instance.metadata.name}/vnc`

The same change is needed in handleReconnect at line 125 (or better, extract both call sites to a single helper — see the duplication comment below).

Also applies to: 125-125

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/console/src/routes/detail/VncTab.tsx` at line 26, The hardcoded wsUrl in
VncTab.tsx (and the duplicate in handleReconnect) uses ws://localhost:8001 which
breaks non-proxy and HTTPS deployments; replace it with a dynamic builder that
derives the scheme from window.location.protocol (use "wss:" when protocol is
"https:"), uses window.location.host or window.location.origin for the host, and
appends the existing path
`/apis/subresources.kubevirt.io/v1/namespaces/${ns}/virtualmachineinstances/${instance.metadata.name}/vnc`;
extract this logic into a single helper (e.g., buildVncWebSocketUrl) and call it
from both the wsUrl initialization and handleReconnect to avoid duplication and
ensure proper wss/ws selection and host resolution.

Comment on lines +118 to +168
const handleReconnect = () => {
// Clear container and reinitialize
if (vncContainerRef.current) {
vncContainerRef.current.innerHTML = ""
setConnecting(true)
setError(null)

const wsUrl = `ws://localhost:8001/apis/subresources.kubevirt.io/v1/namespaces/${ns}/virtualmachineinstances/${instance.metadata.name}/vnc`

import("@novnc/novnc/lib/rfb").then((module) => {
if (!vncContainerRef.current) return

// The module has nested default: module.default.default is the RFB constructor
const RFB = (module as any).default?.default || module.default || module

try {
const rfb = new RFB(vncContainerRef.current, wsUrl, { credentials: {} })
rfb.scaleViewport = true
rfb.resizeSession = false

rfb.addEventListener("connect", () => {
setConnecting(false)
setConnected(true)
setError(null)
})

rfb.addEventListener("disconnect", (e: any) => {
setConnecting(false)
setConnected(false)
if (!e.detail.clean) {
setError(`Disconnected: ${e.detail.reason || "unknown reason"}`)
}
})

rfb.addEventListener("securityfailure", (e: any) => {
setConnecting(false)
setConnected(false)
setError(`Security failure: ${e.detail.status || "authentication failed"}`)
})

rfbRef.current = rfb
} catch (err) {
setConnecting(false)
setError(`Failed to reconnect: ${(err as Error).message}`)
}
}).catch((err) => {
setConnecting(false)
setError(`Failed to load VNC library: ${err.message}`)
})
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

handleReconnect duplicates useEffect init and leaks the previous RFB client.

Two issues in one:

  1. Leak on reconnect. handleReconnect clears vncContainerRef.current.innerHTML but never calls rfbRef.current.disconnect() before constructing a new RFB. The previous client (and its WebSocket / event listeners) is orphaned on every reconnect.
  2. Duplicated init logic. Lines 127–166 re-implement what lines 29–84 already do, so any fix (URL derivation, error handling, typing) has to be made twice. It also lacks the mounted guard, so unmounting during the dynamic import(...) will still call setState.

Suggest extracting a single connectRfb(container) helper and having both the effect and the reconnect handler call it after first disposing rfbRef.current:

🔧 Sketch
+  const connectRfb = async (container: HTMLDivElement, isMounted: () => boolean) => {
+    if (rfbRef.current) {
+      try { rfbRef.current.disconnect() } catch { /* ignore */ }
+      rfbRef.current = null
+    }
+    const scheme = window.location.protocol === "https:" ? "wss" : "ws"
+    const wsUrl = `${scheme}://${window.location.host}/apis/subresources.kubevirt.io/v1/namespaces/${ns}/virtualmachineinstances/${instance.metadata.name}/vnc`
+    const mod = await import("@novnc/novnc/lib/rfb")
+    if (!isMounted()) return
+    const RFB = (mod as any).default?.default ?? (mod as any).default ?? mod
+    const rfb = new RFB(container, wsUrl, { credentials: {} })
+    rfb.scaleViewport = true
+    rfb.resizeSession = false
+    rfb.addEventListener("connect", () => { if (isMounted()) { setConnecting(false); setConnected(true); setError(null) } })
+    rfb.addEventListener("disconnect", (e: any) => {
+      if (!isMounted()) return
+      setConnecting(false); setConnected(false)
+      if (!e.detail?.clean) setError(`Disconnected: ${e.detail?.reason ?? "unknown reason"}`)
+    })
+    rfb.addEventListener("securityfailure", (e: any) => {
+      if (!isMounted()) return
+      setConnecting(false); setConnected(false)
+      setError(`Security failure: ${e.detail?.status ?? "authentication failed"}`)
+    })
+    rfbRef.current = rfb
+  }

Then useEffect and handleReconnect both reduce to calling connectRfb(container, …) inside a try/catch.

🧰 Tools
🪛 ESLint

[error] 131-131: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


[error] 144-144: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


[error] 152-152: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/console/src/routes/detail/VncTab.tsx` around lines 118 - 168,
handleReconnect duplicates the initialization logic from the useEffect and leaks
the previous RFB client by not disposing it; refactor by extracting the shared
connection logic into a single helper (e.g., connectRfb(container: HTMLElement))
that performs the dynamic import, creates RFB, attaches listeners, and respects
the mounted guard, then update both useEffect and handleReconnect to first call
rfbRef.current?.disconnect() (or otherwise dispose the existing client) and then
call connectRfb(vncContainerRef.current); ensure the helper returns/sets
rfbRef.current and that all setState calls are guarded by the mounted flag to
avoid updates after unmount.

Comment on lines +121 to +127
<td className="px-4 py-3">
<Link to={`/console/tenants/${name}/edit`}>
<Button variant="outline" size="sm">
<Edit className="size-3" /> Edit
</Button>
</Link>
</td>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find the edit route definition and loader to confirm the expected param
rg -nP --type=ts --type=tsx -C3 'tenants/[^"'\'']*edit|/tenants/:\w+' apps/console/src 2>/dev/null
rg -nP -C3 'tenantDisplayName' apps/console/src
fd -e ts -e tsx 'Tenant.*Edit|TenantEdit' apps/console/src

Repository: cozystack/cozystack-ui

Length of output: 2718


🏁 Script executed:

#!/bin/bash
# Find route configuration and edit component
find apps/console/src -name "*.tsx" -o -name "*.ts" | grep -E "(route|main|app)" | head -20

# Search for the edit component or loader
rg -l "tenants.*edit" apps/console/src --type=tsx --type=ts

# Look for route definitions with 'edit' pattern
rg -nC5 "path.*edit|:.*edit|tenants/:.*" apps/console/src --type=tsx --type=ts | head -50

# Check if there's a router configuration
fd -e tsx -e ts "router\|route\|app" apps/console/src | head -20

Repository: cozystack/cozystack-ui

Length of output: 1037


🏁 Script executed:

#!/bin/bash
# Search for how the edit route param is used
rg -nC3 "useParams|useSearchParams" apps/console/src --type=tsx --type=ts

# Look for TenantEdit or edit page files
fd -i tenant apps/console/src --type=f | grep -i edit

# Find all route paths
rg -nC2 "path:" apps/console/src --type=tsx --type=ts | head -60

Repository: cozystack/cozystack-ui

Length of output: 122


🏁 Script executed:

#!/bin/bash
# Find route definitions more broadly
rg -n "path.*edit\|:tenantId\|:tenant\|/tenants/" apps/console/src --max-count=30

# Check App.tsx for routes
cat -n apps/console/src/App.tsx | head -100

# Look for useParams usage to see how route params are accessed
rg -n "useParams" apps/console/src -A 2

Repository: cozystack/cozystack-ui

Length of output: 5225


🏁 Script executed:

#!/bin/bash
# Find ConsolePage.tsx and look for route definitions
cat -n apps/console/src/routes/ConsolePage.tsx | head -150

# Search for tenant route definitions
rg -n "tenants" apps/console/src/routes/ConsolePage.tsx -A 3 -B 3

Repository: cozystack/cozystack-ui

Length of output: 4169


🏁 Script executed:

#!/bin/bash
# Check if there's a TenantEditPage or tenant edit component
fd -name "*Tenant*" apps/console/src --type=f

# Search for any tenant edit references
rg -n "TenantEdit\|tenant.*edit" apps/console/src -i

# Check if a catch-all route would catch the tenants/:name/edit path
cat -n apps/console/src/routes/ConsolePage.tsx | tail -20

Repository: cozystack/cozystack-ui

Length of output: 1209


🏁 Script executed:

#!/bin/bash
# Check how ApplicationEditRoute uses the name parameter
cat -n apps/console/src/routes/detail/ApplicationEditRoute.tsx | head -80

Repository: cozystack/cozystack-ui

Length of output: 2138


🏁 Script executed:

#!/bin/bash
# Check if there's a TenantEditPage or any tenant editing functionality
fd "tenant" apps/console/src --type=f -i | xargs grep -l "edit" 2>/dev/null

# Check what routes are currently matching for the tenants/:name/edit path
rg -n "TenantNamespace\|TenantEdit\|tenant.*edit" apps/console/src -i

# Verify the route catch-all behavior - does it try to fetch ApplicationInstance for tenants?
rg -C5 "plural.*tenants" apps/console/src

Repository: cozystack/cozystack-ui

Length of output: 879


🏁 Script executed:

#!/bin/bash
# Check if TenantsPage has edit functionality or if it's trying to route to a non-existent page
cat -n apps/console/src/routes/TenantsPage.tsx | grep -A 5 -B 5 "edit\|Edit"

# Count total lines in TenantsPage.tsx
wc -l apps/console/src/routes/TenantsPage.tsx

# Check if there's any TenantEdit or similar component
find apps/console/src -type f -name "*Tenant*"

Repository: cozystack/cozystack-ui

Length of output: 1263


🏁 Script executed:

#!/bin/bash
# Check what tenantDisplayName actually returns
cat -n apps/console/src/lib/tenant-context.tsx | grep -A 10 "function tenantDisplayName"

# Check the displayName function it calls
cat -n apps/console/src/lib/tenant-context.tsx | grep -B 5 -A 10 "function displayName"

Repository: cozystack/cozystack-ui

Length of output: 847


Edit link uses display name instead of resource identifier.

The link at line 122 uses tenantDisplayName(t) to construct the route parameter. Since tenantDisplayName() strips the "tenant-" prefix from t.metadata.name, this results in an incorrect route param. For example, a tenant with metadata.name: "tenant-example" becomes the URL path /console/tenants/example/edit. When this route is matched by the catch-all pattern :plural/:name/edit, the ApplicationEditRoute will attempt to fetch a resource named "example" in the "tenants" plural, which does not exist—the actual resource is "tenant-example".

Use the canonical identifier t.metadata.name instead:

Proposed fix
-                    <td className="px-4 py-3">
-                      <Link to={`/console/tenants/${name}/edit`}>
+                    <td className="px-4 py-3">
+                      <Link to={`/console/tenants/${encodeURIComponent(t.metadata.name)}/edit`}>
                         <Button variant="outline" size="sm">
                           <Edit className="size-3" /> Edit
                         </Button>
                       </Link>
                     </td>

Note: This fix assumes a dedicated tenant edit route handler exists or will be added to handle the tenants/:name/edit path. Currently, the catch-all ApplicationEditRoute is used, which expects application resources and will not work for tenants.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/console/src/routes/TenantsPage.tsx` around lines 121 - 127, The Edit
link builds the route using the display name (tenantDisplayName(t)) which strips
the "tenant-" prefix and breaks resource lookup; update the Link target to use
the canonical identifier t.metadata.name instead (replace tenantDisplayName(t)
with t.metadata.name in the Link to={`/console/tenants/${...}/edit`} inside
TenantsPage.tsx) so the route param matches the actual resource name expected by
the edit route handler.

ohotnikov.ivan added 5 commits April 24, 2026 12:13
Enable automatic data refresh every 5 seconds on application detail page.
This ensures status updates are visible without manual page reload,
especially important after creating new resources.

Signed-off-by: ohotnikov.ivan <ohotnikov.ivan@e-queo.net>
…ole home

After deleting a resource, redirect to the resource type list page
(e.g., /console/buckets) instead of the console home page.
This provides better UX by keeping the user in the context of the
resource type they were working with.

Signed-off-by: ohotnikov.ivan <ohotnikov.ivan@e-queo.net>
Change Deploy buttons to navigate to /console/marketplace instead of /marketplace
to maintain console navigation context. This prevents users from being
redirected to the standalone marketplace when deploying from console resource lists.

Signed-off-by: ohotnikov.ivan <ohotnikov.ivan@e-queo.net>
Change deploy buttons to navigate to /console/{appName} instead of
/console/marketplace/{appName}. The correct URL structure is:
- From /marketplace → /marketplace/{appName}
- From /console → /console/{appName}

Signed-off-by: ohotnikov.ivan <ohotnikov.ivan@e-queo.net>
Add /console/new/:appName route to allow creating applications
directly from console without switching to marketplace.
This keeps users in the console context and provides consistent UX.

URL structure:
- /marketplace/:appName - create from marketplace
- /console/new/:appName - create from console

Signed-off-by: ohotnikov.ivan <ohotnikov.ivan@e-queo.net>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant